/*Simple forking WWW server benchmark:
 *
 * Return codes:
 *	0 - success
 *	1 - benchmark failed(server is not-line)
 *	2 - bad param
 *	3 - internal error,fork failed
 */





#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <rpc/types.h>
#include <getopt.h>
#include <strings.h>
#include <time.h>
#include <signal.h>
#include "socket.c"


/*values*/
//需要给用户统计出来的信息

volatile int timerexpired = 0;//闹钟超时时间的设置
int speed = 0;//成功连接的个数
int failed = 0;//失败的连接个数
long long bytes = 0;//接受到的字节数

/*globals*/
//HTTP 协议的选择
int http10=1;/* 0-http/0.9, 1-http/1.0, 2-http/1.1*/

/*Allow :GET HEAD OPTIONS TRACE*/
#define METHOD_GET 0//查
#define METHOD_HEAD 1//返回报文头部信息
#define METHOD_OPTIONS 2//询问支持的方法
#define METHOD_TRACE 3//追踪路径

#define PROGRAM_VERSION "1.5"//程序版本的信息

/*default value*/
int method = METHOD_GET;//默认HTTP的方法
int clients = 1;//默认的客户端连接数
int force = 0;//
int force_reload = 0;
int proxyport = 80;//代理端口
char *proxyhost = NULL;
int benchtime = 30;//默认压测时间

/*internal*/
int mypipe[2];
#define MAXHOSTNAMELEN 256
char host[MAXHOSTNAMELEN];
#define REQUEST_SIZE 2048
char request[REQUEST_SIZE];

static const struct option long_options[] = 
{
	{"force",no_argument,&force,1},
	{"reload",no_argument,&force_reload,1},
	{"time",required_argument,NULL,'t'},
	{"help",no_argument,NULL,'?'},
	{"http09",no_argument,NULL,'9'},
	{"http10",no_argument,NULL,'1'},
	{"http11",no_argument,NULL,'2'},
	{"get",no_argument,&method,METHOD_GET},
	{"head",no_argument,&method,METHOD_HEAD},
	{"options",no_argument,&method,METHOD_OPTIONS},
	{"trace",no_argument,&method,METHOD_TRACE},
	{"version",no_argument,NULL,'v'},
	{"proxy",required_argument,NULL,'p'},
	{"clients",required_argument,NULL,'c'},
	{NULL,0,NULL,0}
};


/*prototypes*/
static void benchcore(const char*host,const int port,const char *req);
static int bench(void);
static void build_request(const char *url);

static void alarm_handler(int signal)
{
	timerexpired = 1;
}

static void usage(void)
{
	fprintf(stderr,
	"Welcome  to use this software tool!\n"
	"webbench [option] ... URL\n"
	"	-f|--force					不需要等待服务器的回复\n"
	"	-r|--reload					发送重新加载请求，同时参数为：无缓冲(no-cache)\n"
	"	-t|--time <sec>				压力测试时间，默认30秒\n"
	"	-p|--proxy <server:port>	使用代理服务进行请求\n"
	"	-c|--clients <n>			一次性运行的HTTP客户端数量，默认为1\n"
	"	-9|--http09    				使用HTTP/0.9进行请求\n"
	"	-1|--http10    				使用HTTP/1.0进行请求\n"
	"	-2|--http11    				使用HTTP/1.1进行请求\n"
	"	--get    					使用METHOD_GET\n"
	"	--head    					使用METHOD_HEAD\n"
	"	--options    				使用METHOD_OPTIONS\n"
	"	--trace    					使用METHOD_HEAD\n"
	"	-?|-h|--help    			帮助信息\n"
	"	-v|--version    			版本信息\n"
	);
}

int main(int argc,char *argv[])
{
	int opt = 0;
	int options_index = 0;
	char *tmp = NULL;


	if(argc == 1)
	{
		usage();
			return 2;
	}


	while((opt = getopt_long(argc,argv,"912vfrt:p:c:?h",long_options,&options_index )) != EOF)
	{	
		switch(opt)
		{
			case 0:break;
			case 'f': force=1;break;
			case 'r': force_reload=1;break;
			case '9': http10=0;break;
			case '1': http10=1;break;//当长选项参数和短选项参数一样时，以选项字符串的中的为准
			case '2': http10=2;break;
			case 'v': printf(PROGRAM_VERSION"\n");exit(0);
			case 't': benchtime=atoi(optarg);break;
			case 'p':
					  tmp = strstr(optarg,":");
					  //test
					  /*
					  {
						printf("测试-p 后获取的URL：%s\n",optarg);
						printf("测试获取的IP 和 PORT 分界的':'位置[%s]\n",tmp);
					  }
					  */

					  proxyhost=optarg;
					  if(tmp == NULL)//说明代理地址填写错误
					  {
						  fprintf(stderr,"input URL error\n");
						  break;
					  }
					  if(tmp==optarg)//端口号在optarg开头，说明没有主机号
					  {
							fprintf(stderr,"Error in option --proxy %s:Missing hostname.\n",optarg);
							return 2;

					  }
					  if(tmp==optarg+strlen(optarg)-1)//':'在optarg末尾，说明缺少端口号
					  {
							fprintf(stderr,"Error in option --proxy %s Port number is missing.\n",optarg);
							return 2;

					  }
					  //printf("test->[%c]\n",*tmp);
					  *tmp = '\0';//optarg从':'处截断
					  //printf("test get  port 之前->[%s]\n",tmp+1);
					  proxyport=atoi(tmp+1);/*printf("[%d]-> %d\n",__LINE__,proxyport);*/break;//获取代理服务的端口
			case ':'://缺少选项参数
			case '?'://选项字符不存在
			case 'h': usage();return 2;break;//help
			case 'c': clients = atoi(optarg);break;//获取到要连接的客户端数量

		}
	}


	//解析完命令行参数，读到的位置刚好时URL，optind下标指向将要读的这个URL

	if(optind == argc)//no input URL 
	{
		fprintf(stderr,"webbench:missing URL!\n");
		usage();
		return 2;
	}

	if(clients == 0)
	{
		clients = 1;
	}
	if(benchtime == 0)
	{
		benchtime = 60;
	}

	//开始构建HTTP请求报文，argv[optind]表示URL
	build_request(argv[optind]);


	//构建用户展示信息
	printf("\n正在测试中，请等等...\n");
	switch(method)
	{
		case METHOD_GET:
		default:
			printf("GET");break;
		case METHOD_OPTIONS:
			printf("OPTIONS");break;
		case METHOD_HEAD:
			printf("HEAD");break;
		case METHOD_TRACE:
			printf("TRACE");break;
	}

	printf(" %s",argv[optind]);//output: METHOD + URL 

	switch(http10)
	{
		case 0: printf(" (using HTTP/0.9)");break;
		case 1: printf(" (using HTTP/1.0)");break;
		case 2: printf(" (using HTTP/1.1)");break;
	}

	printf("\n");

	//此时的信息输出：METHOD URL HTTP-VERSION

	//输出准备连接的客户端的数量

	if(clients == 1)
		printf("客户端连接数\t1");
	else
		printf("客户端连接数\t%d",clients);

	printf("\n");

	//输出压测的时间

	printf("压力测试时间：\t%d sec",benchtime);
	printf("\n");

	if(force)//不等待服务器响应数据
		printf("由于你的选择，该次操作会会提前关闭socket\n");
	if(proxyhost != NULL)//使用了代理
		printf("此次代理服务器的IP:%s,PORT:%d\n",proxyhost,proxyport);
	if(force_reload)//使用了缓存的方式，在HTTP/1.1中才会生效
		printf("同时选择无缓存方式!\n");
	printf("\n");


	//以上，提示信息基本完成


	//开始真正的压测
	return bench();

}



void build_request(const char *url)//HTTP请求报文的构建
{
	char tmp[10];
	int i;

	bzero(host,MAXHOSTNAMELEN);
	bzero(request,REQUEST_SIZE);


	//缓冲和代理是在http/1.0后才有的，所以处理一下协议的选择
	//HEAD 也是http/1.0后才有的
	//OPTIONS是http/1.1后才有的
	//TRACE是http/1.1后才有的
	if(force_reload && proxyhost != NULL && http10 < 1)
		http10 = 1;
	if(method == METHOD_HEAD && http10 < 1)
		http10 = 1;
	if(method == METHOD_OPTIONS && http10 < 2)
		http10 = 2;
	if(method == METHOD_TRACE && http10 < 2)
		http10 = 2;

	//开始填写HTTP请求

	//1.1 请求方法

	switch(method)
	{

		default:
		case METHOD_GET: 
			strcpy(request,"GET");break;
		case METHOD_HEAD:
			strcpy(request,"HEAD");break;
		case METHOD_OPTIONS:
			strcpy(request,"OPTIONS");break;
		case METHOD_TRACE:
			strcpy(request,"TRACE");break;
	}

	strcat(request," ");

	//1.2 request URL

	if(NULL == strstr(url,"//"))// 判断URL的有效性
	{
		fprintf(stderr,"\n%s: is not a vaild URL.\n",url);
		exit(2);
	}

	if(strlen(url) > 1500)//判断URL的长度限制
	{
		fprintf(stderr,"URL is too long.\n");
		exit(2);
	}

	//无代理时
	if(proxyhost == NULL)
	{
		if( 0 != strncasecmp("http://",url,7))//忽略大小写比较前七位，支持HTTP
		{
			fprintf(stderr,"\n Only HTTP protocol is directly supported,set --proxy for others.\n");
			exit(2);

		}
	}

	i = strstr(url,"://")-url+3;//7,www.baidu.com

	//判断URL最后一位是否为'/'，判断URL的完整性

	if(strchr(url+i,'/') == NULL)
	{
		fprintf(stderr,"\n Invaild URL syntax - hostname don't ends with '/'.");
		exit(2);

	}

	//若无代理
	if(proxyhost == NULL)
	{
		//有端口号时填写端口
		if(index(url+i,':') != NULL && index(url+i,':') < index(url+i,'/'))//满足有':',同时在'/'的前面
		{
			strncpy(host,url+i,strchr(url+i,':')-url-i);//设置域名或IP
			bzero(tmp,10);

			strncpy(tmp,index(url+i,':')+1,strchr(url+i,'/')-index(url+i,':')-1);//获取端口号

			proxyport = atoi(tmp);
			if(proxyport == 0)
				proxyport = 80;
		}
		else
		{
			strncpy(host,url+i,strcspn(url+i,"/"));
		}

		strcat(request+strlen(request),url+i+strcspn(url+i,"/"));//找到url+i到第一个'/'之间的字符个数
	}
	else//有代理服务
	{
		strcat(request,url);
	}


	//1.3 request URL + version

	if(http10 == 1)
		strcat(request," HTTP/1.0");
	else if(http10 ==2)
		strcat(request," HTTP/1.1");
	else
		strcat(request," HTTP/0.9");
	strcat(request,"\r\n");



	//至此请求报文的状态行填写完成


	//接下来开始填写请求头

	if(http10 > 0)
		strcat(request,"User-Agent: WebBench "PROGRAM_VERSION"\r\n");

	//填写域名或IP
	if(proxyhost == NULL && http10 >0)
	{
		strcat(request,"Host: ");
		strcat(request,host);
		strcat(request,"\r\n");
	}

	//选择强制重新加载，选择无缓存

	if(force_reload && proxyhost != NULL)
	{
		strcat(request,"Pragma: no-cache\r\n");
	}

	//关闭长连接

	if(http10 > 1)
		strcat(request,"Connection: close\r\n");
	//添加空行

	if(http10>0)
		strcat(request,"\r\n");

	//至此构造HTTP请求报文完成，没有填写请求消息主体

}



static int bench(void)
{
	int i,j,k;
	pid_t pid = 0;
	FILE *f;

	//尝试建立连接一次服务器，若不成功，直接退出
	i = Socket(proxyhost == NULL?host:proxyhost,proxyport);
	if(i < 0)
	{
		fprintf(stderr,"\n Connect to server failed.Aborting benchmark.\n");
		return 1;
	}

	close(i);

	//建立父子进程通信管道

	if(pipe(mypipe))
	{
		perror("pipe failed.");
		return 3;
	}

	//fork childs
	//让子进程去测试，子进程数量由clients 决定

	for(i = 0;i<clients;i++)
	{
		pid = fork();
		if(pid <= (pid_t)0)//创建失败或者是子进程
		{		
			sleep(1);
			break;
		}
	}

	if(pid < (pid_t) 0)//fork error
	{
		fprintf(stderr,"problems forking worker NO.%d\n",i);
		perror("fork failed!");
		return 3;
	}

	// 子进程执行流程

	if(pid == (pid_t) 0)
	{
		//child
		if(proxyhost  == NULL)
			benchcore(host,proxyport,request);
		else
			benchcore(proxyhost,proxyport,request);


		//write results to pipe

		f = fdopen(mypipe[1],"w");
		if(f == NULL)
		{
			perror("open pipe for writing failed!");
			return 3;
		}
		

		//写入规定时间内的请求成功次数，失败次数，服务器回复的总字节数

		fprintf(f,"%d %d %lld\n",speed,failed,bytes);
		fclose(f);
		return 0;
	}
	else
	{
		//获得管道读端的文件指针
		f = fdopen(mypipe[0],"r");
		if(f == NULL)
		{	
			perror("open pipe for reading failed!");
			return 3;
		}

		//fopen是标准IO，是自带缓冲的
		//我们输入的数据非常短，为了数据的及时，我们设置成没有缓存的_IONBF
		setvbuf(f,NULL,_IONBF,0);

		speed = 0;
		failed = 0;
		bytes = 0;


		//父进程一直读
		while(1)
		{
			//pid 表示的是成功读到的参数个数
			pid = fscanf(f,"%d %d %d",&i,&j,&k);
			if(pid < 2)
			{
				fprintf(stderr,"Some of our childrens died.\n");
				break;
			}
			speed += i;
			failed += j;
			bytes += k;

			//正常情况下要读的clients次
			if(--clients == 0)
				break;
		}

		fclose(f);

		//统计处理结果

		printf("\n访问速度\t%d pages/min\n每秒服务器回复的字节数\t%d bytes/sec\n连接成功的次数\t%d\n连接失败的总次数\t%d\n",(int)((speed+failed)/(benchtime/60.0f)),(int)(bytes/(float)benchtime),speed,failed);

	}

	return i;
}
//子进程真正的向服务器发出请求报文并获得相关数据

void benchcore(const char *host,const int port,const char *req)
{
	int rlen;
	char buf[1500];
	int s,i;


	
	//安装闹钟信号的处理函数
	struct sigaction sa;
	sa.sa_handler = alarm_handler;
	sa.sa_flags = 0;
	if(sigaction(SIGALRM,&sa,NULL))
		exit(3);
	//设置闹钟函数
	alarm(benchtime);//倒计时时间为压测时间


	rlen = strlen(req);//获得请求报文的总长度
	
nexttry:while(1)
		{
			//只有收到闹钟信号后会使 timerexpired = 1,即该子进程的工作结束了

			if(timerexpired)
			{
				if(failed > 0)
					failed--;//
				return ;
			}

			//建立到目标网站的TCP连接，发起HTTP请求
			s = Socket(host,port);
			if(s < 0)//连接失败
			{
				failed++;
				continue;
			}
			if(rlen != write(s,req,rlen))//写入的不够
			{
				failed++;
				close(s);
				continue;
			}
				

			if(http10 == 0)
			{
				if(shutdown(s,1))
				{
					failed++;
					close(s);
					continue;
				}
			}

			if(force == 0)//-f选项字符没有设置，默认等待服务器的回复
				{
					while(1)
					{
						if(timerexpired)
							break;

						i = read(s,buf,1500);//读服务器发回的数据到buf中
						if(i < 0)
						{
							failed++;
							close(s);
							goto nexttry;
						}
						else if(i == 0)
								break;//read ok
						else
							bytes+=i;//统计服务器回复的字节数
		
					}
				}

	if(close(s))
	{
		failed++;
		continue;
	}
	speed++;

	}
}












